Skip to content

[dotnet] [bidi] Support WebExtension module #15850

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 23 commits into from
Aug 15, 2025

Conversation

nvborisenko
Copy link
Member

@nvborisenko nvborisenko commented Jun 3, 2025

User description

https://w3c.github.io/webdriver-bidi/#module-webExtension

🔗 Related Issues

💥 What does this PR do?

🔧 Implementation Notes

💡 Additional Considerations

🔄 Types of changes

  • Cleanup (formatting, renaming)
  • Bug fix (backwards compatible)
  • New feature (non-breaking change which adds functionality and tests!)
  • Breaking change (fix or feature that would cause existing functionality to change)

PR Type

Enhancement


Description

  • Add WebExtension BiDi module support for .NET

  • Implement install/uninstall extension commands with multiple data formats

  • Add JSON serialization and converter infrastructure

  • Include comprehensive test suite with Bazel integration


Diagram Walkthrough

flowchart LR
  BiDi["BiDi Class"] --> WebExtModule["WebExtensionModule"]
  WebExtModule --> InstallCmd["Install Command"]
  WebExtModule --> UninstallCmd["Uninstall Command"]
  InstallCmd --> ExtData["Extension Data Types"]
  ExtData --> PathExt["Path Extension"]
  ExtData --> ArchiveExt["Archive Extension"]
  ExtData --> Base64Ext["Base64 Extension"]
  Broker["Communication Broker"] --> Converter["WebExtension Converter"]
  Tests["Test Suite"] --> TestData["Bazel Test Data"]
Loading

File Walkthrough

Relevant files
Enhancement
6 files
BiDi.cs
Add WebExtension module property                                                 
+14/-0   
WebExtensionConverter.cs
Create Extension JSON converter                                                   
+47/-0   
Extension.cs
Define Extension entity class                                                       
+40/-0   
InstallCommand.cs
Implement install command with data types                               
+44/-0   
UninstallCommand.cs
Implement uninstall command                                                           
+29/-0   
WebExtensionModule.cs
Create WebExtension module class                                                 
+40/-0   
Configuration changes
5 files
Broker.cs
Register WebExtension JSON converter                                         
+1/-0     
BiDiJsonSerializerContext.cs
Add WebExtension command serialization support                     
+4/-0     
BUILD.bazel
Add dotnet test visibility                                                             
+1/-0     
BUILD.bazel
Include extensions test data                                                         
+1/-0     
Selenium.WebDriver.Common.Tests.csproj
Copy extension files to output                                                     
+4/-0     
Tests
1 files
WebExtensionTest.cs
Add comprehensive WebExtension tests                                         
+96/-0   

@selenium-ci selenium-ci added the C-dotnet .NET Bindings label Jun 3, 2025
Copy link
Contributor

qodo-merge-pro bot commented Jun 3, 2025

PR Reviewer Guide 🔍

Here are some key observations to aid the review process:

🎫 Ticket compliance analysis ❌

1234 - Not compliant

Non-compliant requirements:

• Fix issue where Selenium 2.48 doesn't trigger JavaScript in link's href on click() in Firefox 42.0
• Ensure JavaScript alerts are triggered properly when clicking links with JavaScript in href attribute

5678 - Not compliant

Non-compliant requirements:

• Fix "ConnectFailure (Connection refused)" error when instantiating ChromeDriver multiple times
• Ensure subsequent ChromeDriver instances can be created without connection errors

⏱️ Estimated effort to review: 2 🔵🔵⚪⚪⚪
🧪 PR contains tests
🔒 No security concerns identified
⚡ Recommended focus areas for review

Method Visibility

The UninstallAsync method is marked as internal while it's being called from the public Extension class. Consider making this method public for consistency and to allow direct uninstallation without going through the Extension object.

internal async Task UninstallAsync(Extension extension, UninstallOptions? options = null)
{
    var @params = new UninstallCommandParameters(extension);

    await Broker.ExecuteCommandAsync(new UninstallCommand(@params), options).ConfigureAwait(false);
}

Copy link
Contributor

qodo-merge-pro bot commented Jun 3, 2025

PR Code Suggestions ✨

Explore these optional code suggestions:

CategorySuggestion                                                                                                                                    Impact
Possible issue
Fix inheritance contradiction

The InstallResult class inherits from EmptyResult but also contains an Extension
property, which is contradictory. Either make it a standalone result class or
ensure it properly extends the base class without conflicting behavior.

dotnet/src/webdriver/BiDi/WebExtension/InstallCommand.cs [44]

-public record InstallResult(Extension Extension) : EmptyResult;
+public record InstallResult(Extension Extension);
  • Apply / Chat
Suggestion importance[1-10]: 2

__

Why: The suggestion assumes EmptyResult means no properties are allowed, but this may not be accurate. Removing the inheritance could break expected functionality without understanding the base class purpose.

Low
  • Update

@nvborisenko
Copy link
Member Author

Test fails on CI, because I don't know how to prepare test data in bazel test environment. It passes locally in IDE and dotnet test.

@cgoldberg
Copy link
Contributor

[IgnoreBrowser(Selenium.Browser.Chrome, "Web extensions are not supported yet?")]
[IgnoreBrowser(Selenium.Browser.Edge, "Web extensions are not supported yet?")]

They should work for Chrome/Edge if you start the browser using the flags:

--remote-debugging-pipe
--enable-unsafe-extension-debugging

The Python bindings added a property on Chromium's Options class named enable_webextensions that a user can enable to set these flags.

see #15794

@nvborisenko
Copy link
Member Author

Test fails on CI, because I don't know how to prepare test data in bazel test environment. It passes locally in IDE and dotnet test.

I managed it and now we are able to execute tests in different environments (dotnet test, bazel test, IDE).

@nvborisenko
Copy link
Member Author

--remote-debugging-pipe argument breaks CDP.

#15749 (comment)

@nvborisenko
Copy link
Member Author

Parking it until better weather.

@nvborisenko nvborisenko marked this pull request as draft June 20, 2025 18:48
@nvborisenko
Copy link
Member Author

Here we should refactor tests projects. Any test Fixture should be capable to override the initialization of WebDriver. That's it. Probably relates to #15536

@SKumar-777
Copy link

Hi Team,

Will this option be available in the upcoming Selenium version 4.35?

Thanks.

@cgoldberg
Copy link
Contributor

@SKumar-777 no, sorry... 4.35 was released yesterday and this hasn't been merged yet.

@nvborisenko
Copy link
Member Author

Sorry, I am stuck here. Options to move further:

  • Refactor internal tests infrastructure (big effort)
  • Apply some dirty hack to support this specific case
  • Temporary ignore internal tests and release the functionality

@diemol
Copy link
Member

diemol commented Aug 13, 2025

@navin772 @Delta456 what approach did you take when implementing this?

@cgoldberg
Copy link
Contributor

Refactor internal tests infrastructure (big effort)

what approach did you take when implementing this?

Python tests and fixtures were updated to handle this.

@navin772
Copy link
Member

navin772 commented Aug 13, 2025

@navin772 @Delta456 what approach did you take when implementing this?

The extension related files/dirs had to be copied via bazel, which this PR is already doing.
@nvborisenko I don't have much idea about dotnet but what exactly is blocking this PR? The implementation looks good. Are we blocked on tests, in the last CI run, I see that the related tests are passing.

@nvborisenko
Copy link
Member Author

Ideally to move further we should be able to add arguments for specific fixture/suite/test.

--remote-debugging-pipe
--enable-unsafe-extension-debugging

The issue is that in internal implemented testing infrastructure we cannot do it, only globally, but it breaks DevTools/CDP. Looked at the code, and I don't even see "quick dirty hack" to improve the situation.

Seems our option is to merge without tests, and then add them later when weather becomes good: #15536

@nvborisenko nvborisenko marked this pull request as ready for review August 13, 2025 21:07
Copy link
Contributor

PR Reviewer Guide 🔍

Here are some key observations to aid the review process:

🎫 Ticket compliance analysis 🔶

1234 - Partially compliant

Compliant requirements:

Non-compliant requirements:

  • Investigate regression where click() no longer triggers JavaScript in link href in Selenium 2.48.x (worked in 2.47.1) on Firefox 42.
  • Provide fix or explanation/workaround if applicable.
  • Validate behavior across affected versions/browsers.

Requires further human verification:

5678 - Partially compliant

Compliant requirements:

Non-compliant requirements:

  • Diagnose "ConnectFailure (Connection refused)" when instantiating ChromeDriver after the first instance on Ubuntu 16.04 with Chrome 65/ChromeDriver 2.35.
  • Determine root cause and propose fix or configuration guidance.
  • Provide reproducible steps and ensure subsequent instances work reliably.

Requires further human verification:

⏱️ Estimated effort to review: 2 🔵🔵⚪⚪⚪
🧪 PR contains tests
🔒 No security concerns identified
⚡ Recommended focus areas for review

Null Handling

The converter assumes non-null IDs in Read and Write without null checks. If the protocol returns a null or missing ID, this will throw. Consider validating or throwing a clearer error.

public override Extension? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
{
    var id = reader.GetString();

    return new Extension(_bidi, id!);
}

public override void Write(Utf8JsonWriter writer, Extension value, JsonSerializerOptions options)
{
    writer.WriteStringValue(value.Id);
}
Polymorphic Types

The JsonPolymorphic mapping relies on "type" discriminators. Verify server responses match these strings and that unknown types are handled gracefully to avoid deserialization failures.

[JsonPolymorphic(TypeDiscriminatorPropertyName = "type")]
[JsonDerivedType(typeof(ExtensionArchivePath), "archivePath")]
[JsonDerivedType(typeof(ExtensionBase64Encoded), "base64")]
[JsonDerivedType(typeof(ExtensionPath), "path")]
public abstract record ExtensionData;

public sealed record ExtensionArchivePath(string Path) : ExtensionData;

public sealed record ExtensionBase64Encoded(string Value) : ExtensionData;

public sealed record ExtensionPath(string Path) : ExtensionData;
Skipped Tests

Entire test class is ignored due to driver args conflicts, and some tests are skipped for Chrome/Edge. This limits coverage of the new feature; consider isolating environment to enable execution.

[Ignore("""
    The following test suite wants to set driver arguments via Options, but it breaks CDP/DevTools tests.
    The desired arguments (for Chromium only?):
    --enable-unsafe-extension-debugging
    --remote-debugging-pipe
    Ignoring these tests for now. Hopefully https://github.com/SeleniumHQ/selenium/issues/15536 will be resolved soon.
    """)]
class WebExtensionTest : BiDiTestFixture
{
    [Test]

Copy link
Contributor

qodo-merge-pro bot commented Aug 13, 2025

PR Code Suggestions ✨

Explore these optional code suggestions:

CategorySuggestion                                                                                                                                    Impact
High-level
Validate protocol compatibility

The WebExtension API surface is added without any feature-detection or
capability checks, yet different browsers (and even channels) may not support
archive/base64 installs or the module at all. Add a negotiated
capability/version check via BiDi session status or browsingContext/permissions
before exposing or invoking webExtension.install/uninstall to prevent runtime
protocol errors across engines.

Examples:

dotnet/src/webdriver/BiDi/BiDi.cs [154-165]
public WebExtension.WebExtensionModule WebExtension
{
    get
    {
        if (_webExtensionModule is not null) return _webExtensionModule;
        lock (_moduleLock)
        {
            _webExtensionModule ??= new WebExtension.WebExtensionModule(_broker);
        }
        return _webExtensionModule;

 ... (clipped 2 lines)
dotnet/src/webdriver/BiDi/WebExtension/WebExtensionModule.cs [27-32]
public async Task<InstallResult> InstallAsync(ExtensionData extensionData, InstallOptions? options = null)
{
    var @params = new InstallCommandParameters(extensionData);

    return await Broker.ExecuteCommandAsync<InstallCommand, InstallResult>(new InstallCommand(@params), options).ConfigureAwait(false);
}

Solution Walkthrough:

Before:

// In BiDi.cs
public WebExtension.WebExtensionModule WebExtension
{
    get
    {
        // Lazily initializes module without checking for browser support.
        if (_webExtensionModule is not null) return _webExtensionModule;
        lock (_moduleLock)
        {
            _webExtensionModule ??= new WebExtension.WebExtensionModule(_broker);
        }
        return _webExtensionModule;
    }
}

After:

// In BiDi.cs
// Assumes _supportedModules is populated from session.status on connection.
private HashSet<string> _supportedModules;

public WebExtension.WebExtensionModule WebExtension
{
    get
    {
        if (!_supportedModules.Contains("webExtension"))
        {
            throw new WebDriverException("The WebExtension BiDi module is not supported by this browser.");
        }
        // Lazily initialize module only if supported.
        _webExtensionModule ??= new WebExtension.WebExtensionModule(_broker);
        return _webExtensionModule;
    }
}
Suggestion importance[1-10]: 8

__

Why: This suggestion addresses a significant design oversight by highlighting the lack of protocol compatibility checks, which is crucial for a library intended to work across different browsers.

Medium
Possible issue
Validate JSON token and nulls

Guard against null or non-string JSON tokens to prevent runtime exceptions when
deserializing. Validate the token type and value, and throw a descriptive
JsonException if invalid.

dotnet/src/webdriver/BiDi/Communication/Json/Converters/WebExtensionConverter.cs [36-41]

 public override Extension? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
 {
+    if (reader.TokenType != JsonTokenType.String)
+    {
+        throw new JsonException($"Expected string for Extension id, but got {reader.TokenType}.");
+    }
+
     var id = reader.GetString();
+    if (string.IsNullOrEmpty(id))
+    {
+        throw new JsonException("Extension id cannot be null or empty.");
+    }
 
-    return new Extension(_bidi, id!);
+    return new Extension(_bidi, id);
 }
  • Apply / Chat
Suggestion importance[1-10]: 7

__

Why: This suggestion correctly identifies that the custom JSON converter lacks validation, making it more robust against unexpected or malformed server responses and preventing potential runtime exceptions.

Medium
Align JSON field names

Ensure serialized property names match the protocol schema (camelCase) to avoid
server-side deserialization issues. Add JsonPropertyName attributes to fields to
guarantee correct casing regardless of serializer naming policy.

dotnet/src/webdriver/BiDi/WebExtension/InstallCommand.cs [30-40]

 [JsonPolymorphic(TypeDiscriminatorPropertyName = "type")]
 [JsonDerivedType(typeof(ExtensionArchivePath), "archivePath")]
 [JsonDerivedType(typeof(ExtensionBase64Encoded), "base64")]
 [JsonDerivedType(typeof(ExtensionPath), "path")]
 public abstract record ExtensionData;
 
-public sealed record ExtensionArchivePath(string Path) : ExtensionData;
+public sealed record ExtensionArchivePath([property: JsonPropertyName("path")] string Path) : ExtensionData;
 
-public sealed record ExtensionBase64Encoded(string Value) : ExtensionData;
+public sealed record ExtensionBase64Encoded([property: JsonPropertyName("value")] string Value) : ExtensionData;
 
-public sealed record ExtensionPath(string Path) : ExtensionData;
+public sealed record ExtensionPath([property: JsonPropertyName("path")] string Path) : ExtensionData;
  • Apply / Chat
Suggestion importance[1-10]: 5

__

Why: The suggestion correctly proposes using JsonPropertyName for explicit serialization, which improves code robustness by making it independent of the global serializer naming policy.

Low
General
Add null input validation

Validate input to avoid null reference issues and clearer error messages. Throw
an ArgumentNullException when the required 'extensionData' parameter is null.

dotnet/src/webdriver/BiDi/WebExtension/WebExtensionModule.cs [27-32]

 public async Task<InstallResult> InstallAsync(ExtensionData extensionData, InstallOptions? options = null)
 {
+    if (extensionData is null) throw new ArgumentNullException(nameof(extensionData));
+
     var @params = new InstallCommandParameters(extensionData);
-
     return await Broker.ExecuteCommandAsync<InstallCommand, InstallResult>(new InstallCommand(@params), options).ConfigureAwait(false);
 }
  • Apply / Chat
Suggestion importance[1-10]: 6

__

Why: This suggestion correctly adds a null check for the extensionData parameter, which is a good practice for public API methods to fail early and provide a clear ArgumentNullException.

Low
Learned
best practice
Add constructor argument guards

Guard the constructor against null or empty arguments to ensure the object is
always in a valid state. Throw ArgumentNullException/ArgumentException with
clear messages.

dotnet/src/webdriver/BiDi/WebExtension/Extension.cs [28-32]

 public Extension(BiDi bidi, string id)
 {
-    _bidi = bidi;
-    Id = id;
+    _bidi = bidi ?? throw new ArgumentNullException(nameof(bidi));
+    Id = !string.IsNullOrEmpty(id) ? id : throw new ArgumentException("Extension id cannot be null or empty.", nameof(id));
 }
  • Apply / Chat
Suggestion importance[1-10]: 6

__

Why:
Relevant best practice - Add null checks and validation for parameters and variables before using them to prevent NullReferenceException.

Low
  • More

@nvborisenko
Copy link
Member Author

Let's merge it with the proved assumptions:

  • it works in general
  • test data works, bazel + IDE

Some tests are written, but just ignore for now. Show must go on.

@nvborisenko
Copy link
Member Author

CI failure is not related to these changes. I am merging it because users wait this functionality, even untested.

@nvborisenko nvborisenko merged commit e2bffeb into SeleniumHQ:trunk Aug 15, 2025
24 of 25 checks passed
@nvborisenko nvborisenko deleted the bidi-webextension branch August 15, 2025 21:02
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
None yet
Development

Successfully merging this pull request may close these issues.

6 participants